package com.atguigu.tingshu.search.service.impl;

import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.FieldValue;
import co.elastic.clients.elasticsearch._types.SortOrder;
import co.elastic.clients.elasticsearch._types.aggregations.Aggregate;
import co.elastic.clients.elasticsearch._types.aggregations.LongTermsBucket;
import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.Operator;
import co.elastic.clients.elasticsearch._types.query_dsl.QueryBuilders;
import co.elastic.clients.elasticsearch.core.SearchRequest;
import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.search.CompletionSuggestOption;
import co.elastic.clients.elasticsearch.core.search.Hit;
import co.elastic.clients.elasticsearch.core.search.HitsMetadata;
import co.elastic.clients.elasticsearch.core.search.Suggestion;
import co.elastic.clients.json.JsonData;
import com.atguigu.tingshu.album.client.CategoryFeignClient;
import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.model.album.BaseCategory3;
import com.atguigu.tingshu.model.search.AlbumInfoIndex;
import com.atguigu.tingshu.model.search.SuggestIndex;
import com.atguigu.tingshu.query.search.AlbumIndexQuery;
import com.atguigu.tingshu.search.service.SearchService;
import com.atguigu.tingshu.vo.search.AlbumInfoIndexVo;
import com.atguigu.tingshu.vo.search.AlbumSearchResponseVo;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;

import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;

@Slf4j
@Service
@SuppressWarnings({"unchecked"})
public class SearchServiceImpl implements SearchService {
    @Autowired
    private CategoryFeignClient categoryFeignClient;

    @Autowired
    private ElasticsearchClient elasticsearchClient;

    @Override
    public List<Map<String, Object>> channel(Long category1Id) throws IOException {
        //根据一级分类id查询置顶的七个三级分类
        Result<List<BaseCategory3>> topBaseCategory3Result = this.categoryFeignClient.findTopBaseCategory3(category1Id);
        Assert.notNull(topBaseCategory3Result,"首页置顶频道,获取置顶的三级分类失败!");
        //7个三级分类的集合
        List<BaseCategory3> baseCategory3s = topBaseCategory3Result.getData();
        if (CollectionUtils.isEmpty(baseCategory3s)){
            return null;
        }
        //把三级分类集合转化成Map<category3Id, BaseCategory3>
        Map<Long, BaseCategory3> category3Map = baseCategory3s.stream().collect(Collectors.toMap(BaseCategory3::getId, baseCategory3 -> baseCategory3));

        //把三级分类集合处理成FiledValue集合
        List<FieldValue> category3Ids = baseCategory3s.stream().map(baseCategory3 -> FieldValue.of(baseCategory3.getId())).collect(Collectors.toList());
        //构建搜索的Dsl语句

        SearchRequest request = SearchRequest.of(s -> s.index("albuminfo")
                .size(0)
                .query(q -> q.terms(t -> t.field("category3Id").terms(ts -> ts.value(category3Ids))))
                .aggregations("categoryAgg", a -> a.terms(t -> t.field("category3Id"))
                        .aggregations("hotScoreTop", agg -> agg.topHits(t -> t.size(6)
                                .sort(st -> st.field(f -> f.field("hotScore").order(SortOrder.Desc)))))
                ));
        System.out.println(request);

        //执行搜索获取置顶频道的对应数据
        SearchResponse<AlbumInfoIndex> response = this.elasticsearchClient.search(request, AlbumInfoIndex.class);
        //解析结果集
        Map<String, Aggregate> aggregations = response.aggregations();
        if (CollectionUtils.isEmpty(aggregations)){
            return null;
        }
        //获取分类集合
        Aggregate aggregate = aggregations.get("categoryAgg");
        if (aggregate==null){
            return null;
        }
        //获取聚合中桶的集合
        List<LongTermsBucket> buckets = aggregate.lterms().buckets().array();
        return buckets.stream().map(bucket ->{
            Map<String,Object> map = new HashMap<>();
            //根据三级分类id获取三级分类
          map.put("baseCategory3", category3Map.get(bucket.key()));
          //获取桶中的子聚合
            Aggregate sugAgg = bucket.aggregations().get("hotScoreTop");
            //获取子聚合中的top榜数据
            List<Hit<JsonData>> hitList = sugAgg.topHits().hits().hits();
            //把List<Hit<JsonData>>转化为List<AlbumInfoIndex>
            map.put("list",hitList.stream().map(hit ->hit.source().to(AlbumInfoIndex.class)).collect(Collectors.toList()));
            return map;

        }).collect(Collectors.toList());
    }

    @Override
    public AlbumSearchResponseVo search(AlbumIndexQuery query) {

        try {
            SearchResponse<AlbumInfoIndex> response = this.elasticsearchClient.search(this.buildDsl(query), AlbumInfoIndex.class);
            AlbumSearchResponseVo responseVo = this.parseResult(response);
            // 设置分页参数：只有请求参数中才有分页数据
            responseVo.setPageNo(query.getPageNo());
            responseVo.setPageSize(query.getPageSize());
            long totalPage = responseVo.getTotal() / responseVo.getPageSize();
            responseVo.setTotalPages(responseVo.getTotal() % responseVo.getPageSize() == 0 ? totalPage : totalPage + 1);

            return responseVo;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

   @Override
    public List<String> completeSuggest(String keyword) {
        SearchRequest request = SearchRequest.of(s -> s.index("suggestinfo")
                .suggest(sg -> sg
                        .suggesters("keywordSuggest", sug -> sug
                                .prefix(keyword)
                                .completion(c -> c
                                        .field("keyword")
                                        .size(10)
                                        .skipDuplicates(true)))
                        .suggesters("keywordPySuggest", sug -> sug
                                .prefix(keyword)
                                .completion(c -> c
                                        .field("keywordPinyin")
                                        .size(10)
                                        .skipDuplicates(true)))
                        .suggesters("keywordSeqSuggest", sug -> sug
                                .prefix(keyword)
                                .completion(c -> c
                                        .field("keywordSequence")
                                        .size(10)
                                        .skipDuplicates(true))))
        );
        System.out.println(request);
        try {
            SearchResponse<SuggestIndex> response = this.elasticsearchClient.search(request, SuggestIndex.class);
            // 获取suggest结果集
            Map<String, List<Suggestion<SuggestIndex>>> suggestMap = response.suggest();
            // 获取三个suggest中的options
            List<CompletionSuggestOption<SuggestIndex>> keywordSuggest = suggestMap.get("keywordSuggest").get(0).completion().options();
            List<CompletionSuggestOption<SuggestIndex>> keywordPySuggest = suggestMap.get("keywordPySuggest").get(0).completion().options();
            List<CompletionSuggestOption<SuggestIndex>> keywordSeqSuggest = suggestMap.get("keywordSeqSuggest").get(0).completion().options();

            // 定义一个集合统一接受suggest提示词
            List<String> suggests = new ArrayList<>();

            // 解析中文
            if (!CollectionUtils.isEmpty(keywordSuggest)){
                keywordSuggest.forEach(suggest -> {
                    suggests.add(suggest.source().getTitle());
                });
            }

            // 解析拼音
            if (!CollectionUtils.isEmpty(keywordPySuggest)){
                keywordPySuggest.forEach(suggest -> {
                    suggests.add(suggest.source().getTitle());
                });
            }

            // 解析首字母
            if (!CollectionUtils.isEmpty(keywordSeqSuggest)){
                keywordSeqSuggest.forEach(suggest -> {
                    suggests.add(suggest.source().getTitle());
                });
            }

            // 对数据进行去重：拼音和首字母的提示词可能会出现重复的
            Set<String> set = Set.copyOf(suggests);
            suggests.clear();
            suggests.addAll(set);

            // 判断提示词集合中的数据是否太少了，如果太少，可以进行搜索
            if (suggests.size() >= 10){
                return suggests;
            }
            SearchResponse<SuggestIndex> resp = this.elasticsearchClient.search(s -> s
                    .index("suggestinfo")
                    .query(q -> q.match(m -> m.field("title").query(keyword))), SuggestIndex.class);
            List<Hit<SuggestIndex>> hitList = resp.hits().hits();
            if (!CollectionUtils.isEmpty(hitList)){
                hitList.forEach(hit -> {
                    if (suggests.size() < 10){
                        SuggestIndex suggestIndex = hit.source();
                        suggests.add(suggestIndex.getTitle());
                    }
                });
            }

            return suggests;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public List<AlbumInfoIndexVo> findRankingList(Long category1Id, String type) {

        try {
            SearchResponse<AlbumInfoIndex> response = this.elasticsearchClient.search(s -> s.index("albuminfo")
                    .query(q -> q.term(t -> t.field("category1Id").value(category1Id)))
                    .sort(st -> st.field(f -> f.field(type).order(SortOrder.Desc)))
                    .size(20), AlbumInfoIndex.class);

            List<Hit<AlbumInfoIndex>> hitList = response.hits().hits();
            if (CollectionUtils.isEmpty(hitList)){
                return null;
            }

            return hitList.stream().map(hit -> {
                AlbumInfoIndex albumInfoIndex = hit.source();
                AlbumInfoIndexVo albumInfoIndexVo = new AlbumInfoIndexVo();
                BeanUtils.copyProperties(albumInfoIndex, albumInfoIndexVo);
                return albumInfoIndexVo;
            }).collect(Collectors.toList());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 通过用户的查询条件构建出DSL语句
     *
     * @param query
     * @return
     */
    private SearchRequest buildDsl(AlbumIndexQuery query) {
        SearchRequest.Builder requestBuilder = new SearchRequest.Builder();
        requestBuilder.index("albuminfo");

        // 1.构建出查询过滤
        BoolQuery.Builder boolQueryBuilder = QueryBuilders.bool();
        // 1.1.如果关键字不为空，需要放入多字段匹配
        String keyword = query.getKeyword();
        if (StringUtils.isNotBlank(keyword)) {
            boolQueryBuilder.must(m -> m.multiMatch(mm -> mm
                    .fields("albumTitle",
                            "albumIntro",
                            "announcerName")
                    .query(keyword)
                    .operator(Operator.And)));
        }
        // 1.2.构建过滤条件
        // 1.2.1. 一级分类过滤
        Long category1Id = query.getCategory1Id();
        if (category1Id != null) {
            boolQueryBuilder.filter(f -> f.term(t -> t
                    .field("category1Id")
                    .value(category1Id)));
        }

        // 1.2.2. 二级分类过滤
        Long category2Id = query.getCategory2Id();
        if (category2Id != null) {
            boolQueryBuilder.filter(f -> f.term(t -> t
                    .field("category2Id")
                    .value(category2Id)));
        }

        // 1.2.3. 三级分类过滤
        Long category3Id = query.getCategory3Id();
        if (category3Id != null) {
            boolQueryBuilder.filter(f -> f.term(t -> t
                    .field("category3Id")
                    .value(category3Id)));
        }

        // 1.2.4. 标签的嵌套过滤
        List<String> attributeList = query.getAttributeList();
        if (!CollectionUtils.isEmpty(attributeList)) { // ["2:3", "3:6"]
            attributeList.forEach(attr -> { // 2:3
                String[] attrs = StringUtils.split(attr, ":");
                if (attrs == null || attrs.length != 2 || !StringUtils.isNumeric(attrs[0]) || !StringUtils.isNumeric(attrs[1])) {
                    return;
                }
                boolQueryBuilder.filter(f -> f.nested(n -> n
                        .path("attributeValueIndexList")
                        .query(q -> q.bool(b -> b
                                .must(m -> m.term(t -> t.field("attributeValueIndexList.attributeId").value(attrs[0])))
                                .must(m -> m.term(t -> t.field("attributeValueIndexList.valueId").value(attrs[1])))))));
            });
        }
        requestBuilder.query(boolQueryBuilder.build()._toQuery());

        // 2.构建出排序  2:desc
        // 排序（综合排序[1:desc] 播放量[2:desc] 发布时间[3:desc]；asc:升序 desc:降序）
        String order = query.getOrder();
        if (StringUtils.isNotBlank(order)) {
            String[] sorts = StringUtils.split(order, ":");
            if (sorts != null && sorts.length == 2 && StringUtils.isNumeric(sorts[0])
                    && List.of("desc", "asc").contains(sorts[1])) {
                String fieldName = "_score";
                switch (sorts[0]) {
                    case "1":
                        fieldName = "hotScore";
                        break;
                    case "2":
                        fieldName = "playStatNum";
                        break;
                    case "3":
                        fieldName = "createTime";
                        break;
                }
                String finalFieldName = fieldName;
                requestBuilder.sort(s -> s.field(f -> f
                        .field(finalFieldName)
                        .order(StringUtils.equals(sorts[1], "desc") ? SortOrder.Desc : SortOrder.Asc)));
            }
        }

        // 3.构建出分页
        Integer pageNo = query.getPageNo();
        Integer pageSize = query.getPageSize();
        requestBuilder.from((pageNo - 1) * pageSize);
        requestBuilder.size(pageSize);

        // 4.构建高亮：只有搜索关键字不为空，才需要进行高亮
        if (StringUtils.isNotBlank(keyword)) {
            requestBuilder.highlight(h -> h
                    .fields("albumTitle", f -> f
                            .preTags("<font style='color:red'>")
                            .postTags("</font>"))
                    .fields("albumIntro", f -> f
                            .preTags("<font style='color:red'>")
                            .postTags("</font>")));
        }

        // 5.构建结果集过滤
        requestBuilder.source(s -> s.filter(f -> f.excludes("category1Id",
                "category2Id",
                "category3Id",
                "createTime",
                "hotScore")));

        SearchRequest request = requestBuilder.build();
        System.out.println(request);
        return request;
    }

    /**
     * 把搜索结果集解析成页面所需的结果集
     *
     * @param response
     * @return
     */
    private AlbumSearchResponseVo parseResult(SearchResponse<AlbumInfoIndex> response) {
        AlbumSearchResponseVo responseVo = new AlbumSearchResponseVo();

        // 获取搜索结果集中hits
        HitsMetadata<AlbumInfoIndex> hits = response.hits();
        responseVo.setTotal(hits.total().value()); // 总记录数

        // 获取内层的hits
        List<Hit<AlbumInfoIndex>> hitList = hits.hits();
        // 把Hit集合转化成List<AlbumInfoIndexVo>集合
        responseVo.setList(hitList.stream().map(hit -> {
            AlbumInfoIndexVo albumInfoIndexVo = new AlbumInfoIndexVo();
            // 获取_source中的AlbumInfoIndex
            AlbumInfoIndex albumInfoIndex = hit.source();
            BeanUtils.copyProperties(albumInfoIndex, albumInfoIndexVo);
            // 获取高亮结果集，取代普通的结果集
            Map<String, List<String>> highlight = hit.highlight();
            List<String> albumTitle = highlight.get("albumTitle");
            if (!CollectionUtils.isEmpty(albumTitle)){
                albumInfoIndexVo.setAlbumTitle(albumTitle.get(0));
            }
            List<String> albumIntro = highlight.get("albumIntro");
            if (!CollectionUtils.isEmpty(albumIntro)){
                albumInfoIndexVo.setAlbumIntro(albumIntro.get(0));
            }
            return albumInfoIndexVo;
        }).collect(Collectors.toList()));

        return responseVo;
    }
}
